<?php
###########################################################################
#  Copyright (C) 2006-2007 Glyphix, Inc. <briz@glyphix.com>               #
#                                                                         #
#  Permission is hereby granted, free of charge, to any person obtaining  #
#  a copy of this software and associated documentation files (the        #
#  "Software"), to deal in the Software without restriction, including    #
#  without limitation the rights to use, copy, modify, merge, publish,    #
#  distribute, sublicense, and/or sell copies of the Software, and to     #
#  permit persons to whom the Software is furnished to do so, subject to  #
#  the following conditions:                                              #
#                                                                         #
#  The above copyright notice and this permission notice shall be         #
#  included in all copies or substantial portions of the Software.        #
#                                                                         #
#  Except as contained in this notice, the name(s) of the above           #
#  copyright holders shall not be used in advertising or otherwise to     #
#  promote the sale, use or other dealings in this Software without       #
#  prior written authorization.                                           #
#                                                                         #
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        #
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     #
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. #
#  IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR      #
#  OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,  #
#  ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR  #
#  OTHER DEALINGS IN THE SOFTWARE.                                        #
###########################################################################

/**
 * GXOutput
 *
 * GXPage uses GXOutput to build the html header and to send the actual data back to GXPage.
 * @author		Brad Brizendine <briz@glyphix.com>
 * @link		http://www.glyphix.com/
 * @license		http://opensource.org/licenses/bsd-license.php BSD License
 * @version		1.0
 * @package		GXPage
 * @subpackage	Engine
 * @copyright	Copyright 2006 - 2007, Glyphix, Inc.
 * @uses		SingletonPattern
 */

class GXOutput extends SingletonPattern {

	// mime object
	public $mime;

	// default output mime type
	private $Output = 'HTML';
	// standard header array
	private $OutputHeader;
	// filename, which can be set by any script
	private $FileName;
	// browser sniff info
	public $UserAgent;
	public $BrowserSniff;

	/**
	 * getInstance
	 *
     * Returns the single unique instance of this class.
	 * @return object unique instance of this object
     */
	public static function getInstance(){
		return SingletonPattern::getSingleton( __CLASS__ );
	}

	function __construct(){
		return true;
	}

	function setOutput( $out = 'HTML' ){
		$this->Output = $out;
	}

	function setFileName( $name = null ){
		$this->FileName = $name;
		$this->Output = 'File';
		return $this->FileName;
	}

	function getXMLString( $source = null ){
		switch( get_class($source) ){
			case 'GXDOM':
				$xml = $source->exec->xml();
				break;
			case 'DOM':
			case 'DOMDocument':
				$xml = $source->saveXML();
				break;
			case 'SimpleXML':
				$xml = $source->asXML();
				break;
			default:
				trigger_error( 'Unsupported object type', E_USER_ERROR );
				return false;
		}
		return $xml;
	}

	/**
	 * process
	 *
	 * Handles outputting a result: performs XSLT transformation if necessary and sets the output header.
	 * @param string $mime the document type (HTML, XML, FILE, RAWHTML, RAWXML)
	 * @param object $source the XML payload
	 * @param mixed $sheet the XSL stylesheet (either string or object)
	 * @param boolean $transform whether to process the XML payload with XSLT
	 * @return mixed or false on failure
	 */
	function process( $mime = null, $source = null, $sheet = null, $transform = true ){
		$this->mime = new GXDOM( dirname(__FILE__) .DIRECTORY_SEPARATOR .'Data' .DIRECTORY_SEPARATOR .'mime.xml' );
		$mimeType = strtolower(trim($mime));
		// Set default output header values
		$this->OutputHeader = array(
			'Content-Type'		=> $this->mime->exec->xpath('/mime/*[ @code = "html" ]',0)->nodeValue,
			'Cache-Control'		=> 'no-cache',
			'Pragma'			=> 'no-cache',
			'Content-Language'	=> GXPage::get('Params')->getLanguage()
			);

		// Default mime...it might come in as NULL
		if( !$mimeType ){
			$mimeType = $this->Output;
		}
		// if we're transforming and we have no stylesheet, stop
		if( $mimeType != 'xml' && $mimeType != 'raw' && ($transform && !$sheet) ){
			trigger_error( 'Missing StyleSheet; cannot continue', E_USER_ERROR );
			return false;
		}

		// init xml var
		$xml = null;
		$result = null;

		// if we're transforming ...
		if( $transform ){
			// try to get a string
			$str = null;
			if( !($str = $this->getXMLString($source)) ){
				trigger_error('Unable to get XML string from the provided source::gettype = [' .gettype($source) .']', E_USER_ERROR);
				return $this->process('XML', $source, $sheet, false);
			}
			// if we have a failure on transformation, return everything as xml
			if( !($result = $this->transform($str, $sheet)) ){
				trigger_error( 'Transformation failed, calling self again setting transform to false', E_USER_WARNING );
				return $this->process('XML', $source, $sheet, false);
			}
		}else{
			$result = $source;
		}

		// now handle the header
		switch( $mimeType ){

			case 'text':
			case 'rawtext':
				// if we want rawtext, get rid of the beginning doctype, since it interferes with javascript parsing
				if( $mimeType == 'rawtext' ){
					$result = preg_replace('/<!DOCTYPE.*?.dtd">/','',$result);
				}
				$this->OutputHeader['Content-Type']					= 'text/plain';
				$this->OutputHeader['Content-Length']				= strlen($result);
				break;

			case 'html':
			case 'rawhtml':
				/*
				// if we just processed an action that was supposed to be a file but we receive gxdom instead,
				// we might end up here, since the default mimetype is "html".
				// if we still have untransformed gxdom, have this class spit out xml.
				if( get_class($result) == 'GXDOM' ){
					return $this->process( 'XML', $result, $sheet, false );
				}
				*/
				// if we want rawhtml, get rid of the beginning doctype, since it interferes with javascript parsing
				if( $mimeType == 'rawhtml' ){
					$result = preg_replace('/<!DOCTYPE.*?.dtd">/','',$result);
				}
				$this->OutputHeader['Content-Length']				= strlen($result);
				break;

			case 'json':
				// if we want rawtext, get rid of the beginning doctype, since it interferes with javascript parsing
				$result = preg_replace('/<!DOCTYPE.*?.dtd">/','',$result);
				$result = preg_replace('/<\?xml.*?\?>/','',$result);
				$this->OutputHeader['Content-Type']					= 'application/json';
				$this->OutputHeader['Content-Length']				= strlen($result);
				break;

			case 'raw':
			case 'rawraw':
				/*
				$result = $source;
				// we're expecting a string; if it's not, try to get one
				if( gettype($result) != 'string' ){
					if( !($result = $this->getXMLString($result)) ){
						trigger_error( 'Output requested RAW but received ' .gettype($source) .' and unable to convert to string', E_USER_ERROR );
						$result = 'ERROR';
					}
				}
				*/
				// get rid of the beginning doctype
				$result = preg_replace('/<!DOCTYPE.*?.dtd">/','',$result);
				$result = preg_replace('/<\?xml.*?\?>/','',$result);
				$this->OutputHeader['Content-Length']				= strlen($result);
				break;

			case 'xml':
			case 'rawxml':
				if( is_object($result) ){
					$result = $this->getXMLString($result);
				}
				$this->OutputHeader['Content-Type']					= $this->mime->exec->xpath('/mime/*[ @code = "xml" ]',0)->nodeValue;
				$this->OutputHeader['Content-Length']				= strlen($result);
				break;

			case 'file':
			case 'rawfile':
				/*
				$result = $source;
				// if we have an object, try to get a string
				if( is_object($result) ){
					$result = $this->getXMLString($result);
				}
				*/
				$fileName = $this->FileName;
				if( !$fileName ){
					$fileName = 'GXPage-' .date('Ymd') .'txt';
				}
				$ext = substr($fileName, strrpos($fileName, '.') + 1);
				$mimeType = $this->mime->exec->xpath('/mime/*[ @code = "' .strtolower($ext) .'" ]',0);
				// if we didn't get anything, default to binary
				$this->OutputHeader['Content-Type'] = is_object($mimeType) ? $mimeType->nodeValue : 'binary/unknown';

				// Sanitize the filename...make it safe for OS digestion
				$tmp = ereg_replace("\.[\.]+", "", $fileName);
				$tmp = ereg_replace("^[\/]+", "", $tmp);
				$tmp = ereg_replace("^[a-zA-Z][:\|][\/]?", "", $tmp);
				$fileName = $tmp;

				// When downloading a file, be careful of settings for Expires.
				// IE will save the file if expires is set to now, but will not allow the user to automatically open it.
				// It appears that IE respects the Expires setting so rigidly that once the file is downloaded, the cache deletes it before it can be opened.
				$this->OutputHeader['Pragma']						= 'cache';
				$this->OutputHeader['Cache-Control']				= 'public, must-revalidate, max-age=0';
				$this->OutputHeader['Connection']					= 'close';
				$this->OutputHeader['Expires']						= date("r", time() + (60 * 5));
				$this->OutputHeader['Last-Modified']				= date("r", time());
				$this->OutputHeader['Content-Disposition']			= 'attachment; filename="' .$fileName .'"';
				$this->OutputHeader['Content-Transfer-Encoding']	= 'binary';
				$this->OutputHeader['Content-Length'] 				= strlen($result);
				break;

			default:
				trigger_error( 'Output value incorrect: ' .$this->Output, E_USER_ERROR );
				return false;

		}

		// Handle the header
		if( !headers_sent() ){
			$this->setHeader();
		}
		return $result;
	}

	function transform( $xml = null, $sheet = null ){
		trigger_error( 'About to transform using ' .$sheet, E_USER_WARNING );

		$this->GXXSL = GXXSL::getInstance();
		$this->GXXSL->setEncoding( GXPage::get('Run')->Encoding );

		if( !($result = $this->GXXSL->process( $xml, $sheet )) ){
			trigger_error( 'Unable to transform with ' .$sheet .'; check your web server error log for more detailed information, or set Debug to 1 in SystemConfig', E_USER_ERROR );
			trigger_error( 'System error on XSL transformation; cannot continue.', E_USER_NOTICE );
			return false;
		}
		return $result;
	}

	function setHeader(){
		if( !count($this->OutputHeader) ){
			trigger_error( 'No output header items', E_USER_ERROR );
			return false;
		}
		// update Content-Type to include encoding
		$encoding = GXPage::get('Run')->Encoding;
		if($encoding){
			$this->OutputHeader['Content-Type'] = $this->OutputHeader['Content-Type'] .';charset=' .$encoding;
		}

		trigger_error( 'Setting header to ' .print_r($this->OutputHeader,true), E_USER_WARNING );

		foreach( $this->OutputHeader as $name => $val ){
			header( $name .': ' .$val );
		}
		return true;
	}

	/**
	 * sniffBrowser
	 *
	 * Pulls engine, browser, major and minor versions from the useragent string.
	 * @return array
	 */
	public function sniffBrowser(){
		if($this->BrowserSniff){
			return $this->BrowserSniff;
		}

		// get the browser and its major and minor version
		$browser = null;
		$agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'LOCAL';

		/*
		preg_match_all('/(?<browser>mozilla|firefox|safari|applewebkit|webkit|microsoft internet explorer|msie|netscape|netscape6|opera|konqueror)[\/\sa-z(](?<major>[0-9]+)(?<minor>[\.0-9a-z]+)?/i',
								   $_SERVER['HTTP_USER_AGENT'],
								   $browser);
		*/
		// media temple's php doesn't support naming of internal regex parts, so build it by hand
		preg_match_all('/(mozilla|firefox|chrome|safari|applewebkit|webkit|microsoft internet explorer|msie|netscape|netscape6|opera|konqueror)[\/\sa-z(]([0-9]+)([\.0-9a-z]+)?/i',
							$agent,
							$browser);

		$browser = array(
				'sig'		=> $browser[0],
				'browser'	=> $browser[1],
				'major'		=> $browser[2],
				'minor'		=> $browser[3]
		);

		// get the platform
		$platform = null;
		preg_match_all('/(windows|iphone|ipad|mobile|macintosh|linux)/i', $agent, $platform);
		$browser['platform'] = $platform;
		// add the full string
		$browser['full'] = $agent;

		// if we're a local request, there is no http_user_agent, so fake the variables
		if( ! isset($_SERVER['HTTP_USER_AGENT'])){
			$platform[0][0] = 'LOCAL';
			$platform[0][1] = 'SERVER';
			$browser['browser'][1] = 'LOCAL';
			$browser['major'][1] = '1';
			$browser['minor'][2] = '0';
		}

		// create an array of important browser-related info
		$this->BrowserSniff = array(
			'Engine'		=> array(
				'name'		=> trim($browser['browser'][1]),
				'version'	=> trim($browser['major'][1])
				),
			'Browser'		=> array(
				'name'		=> array_pop($browser['browser']),
				'major'		=> array_pop($browser['major']),
				'minor'		=> array_pop($browser['minor'])
				),
			'Platform'		=> array(
				'name'		=> trim($platform[0][0])
				),
			'full'			=> $agent
			);

		return $this->BrowserSniff;
	}

	/**
	 * CanXSL
	 *
     * Determines if the current browser supports client-side XML transformation.
	 *
	 * @returns Boolean
     */
	function canXSL(){
		// get the browser sniff info
		$profile = $this->sniffBrowser();
		// determine whether this browser can transform xsl live
		switch(true){
			// Opera
			case $profile['Browser']['name'] == 'Opera':
				trigger_error( 'Browser is Opera (' .$_SERVER['HTTP_USER_AGENT'] .')', E_USER_WARNING );
				$XSLOK = false;
				break;
			// IE 6 and up
			case $profile['Browser']['name'] == 'MSIE' && $profile['Browser']['major'] >= 6:
				trigger_error( 'Browser is IE 6 or greater (' .$_SERVER['HTTP_USER_AGENT'] .')', E_USER_WARNING );
				$XSLOK = true;
				break;
			// Mozilla
			case $profile['Browser']['name'] == 'Firefox':
				trigger_error( 'Browser is Mozilla (' .$_SERVER['HTTP_USER_AGENT'] .')', E_USER_WARNING );
				$XSLOK = true;
				break;
			// Safari
			case $profile['Browser']['name'] == 'Safari':
				trigger_error( 'Browser is Safari (' .$_SERVER['HTTP_USER_AGENT'] .')', E_USER_WARNING );
				$XSLOK = true;
				break;
			// Chrome
			case $profile['Browser']['name'] == 'Chrome':
				trigger_error( 'Browser is Chrome (' .$_SERVER['HTTP_USER_AGENT'] .')', E_USER_WARNING );
				$XSLOK = true;
				break;
			// Presumed to not support xsl
			default:
				trigger_error( 'Browser is not known to support XSL transformation (' .$_SERVER['HTTP_USER_AGENT'] .')', E_USER_WARNING );
				$XSLOK = false;
		}
		$this->XSLOK = $XSLOK;
		$this->Browser = $_SERVER['HTTP_USER_AGENT'];
		return $XSLOK;
	}

}

?>